package fcl.platform;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.IBinder;
import android.os.RemoteCallbackList;
//s import android.telephony.CellLocation;
//s import android.telephony.TelephonyManager;
//s import android.telephony.gsm.GsmCellLocation;
//s import android.telephony.cdma.CdmaCellLocation;
import android.widget.Toast;

import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D;
import fcl.platform.R;

class MyPowerManager {
	PowerManager.WakeLock wl;

	MyPowerManager(Context ctx, int power_type) {
		PowerManager pm = (PowerManager) ctx
				.getSystemService(Context.POWER_SERVICE);
		wl = pm.newWakeLock(power_type, "MyPowerManager");
	}

	public void stop() {
		if (wl.isHeld()) {
			wl.release();
		}
	}

	public boolean start() {
		if (!wl.isHeld()) {
			wl.acquire();
		}
		return true;
	}
}

class MySensorManager {

	private SensorManager sensorManager;
	private SensorEventListener listener;
	private int type;
	private boolean running = false;
	private Context ctx;

	MySensorManager(Context ctx, int sensor_type, SensorEventListener listener) {  
		sensorManager = (SensorManager) ctx
				.getSystemService(Context.SENSOR_SERVICE);
		this.listener = listener;
		type = sensor_type;
		this.ctx = ctx;
	}

	public void stop() {
		if (running) {
			running = false;
			try {
				sensorManager.unregisterListener(listener);
			} catch (Exception e) {
			}
		}
	}

	public boolean start() {
		if (!running) {
			List<Sensor> sensors = sensorManager.getSensorList(type);
			if (sensors.size() > 0) {
				Sensor sensor = sensors.get(0);
				
				
				int s_update_interval=1500000; //???
				
				
				running = sensorManager.registerListener(this.listener, sensor, s_update_interval);
			} else {
				Toast.makeText(ctx, "no sensor for type " + type,
						Toast.LENGTH_SHORT).show();
				return false;
			}
		}
		if (!running) {
			Toast.makeText(ctx, R.string.sensor_err, Toast.LENGTH_SHORT).show();
		}
		return running;
	}
}

class MyLocationManager {
	private LocationManager locationManager;
	private LocationListener listener;
	private boolean running = false;
	private final int update_interval;
	
	MyLocationManager(Context ctx, LocationListener listener, int update_interval) {
		this.listener = listener;
		locationManager = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
		this.update_interval = update_interval;
	}

	public void stop() {
		if (running) {
			running = false;
			try {
				locationManager.removeUpdates(listener);
			} catch (Exception e) {
			}
		}
	}

	public boolean start() {
		if (!running) {
			locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, update_interval * 1500, 0, listener);
		}
		return running;
	}
}

class FileWriter implements Runnable {
	private FileOutputStream out;
	private final BlockingQueue<byte[]> msg_q;
	private String file_name;
	private final String folder_name;
	private final int INITIAL_RETRY_TIME = 5;
	private final int MAX_RETRY_TIME = 60;

	public FileWriter(BlockingQueue<byte[]> msg_q, String folder) {
		this.msg_q = msg_q;
		this.folder_name = folder;
	}

	public boolean open(String name) {
		file_name = name;
		return open();
	}

	private boolean open() {
		if (out != null) {
			return true;
		}
		
		File root = new File(Environment.getExternalStorageDirectory() + folder_name);
		if(!root.exists()) {
			root.mkdirs();
		}
		if (root.canWrite()) {
			try {
				out = new FileOutputStream(root + "/" + file_name, true);
				return true;
			} catch (FileNotFoundException e) {
			}
		}
		return false;
	}

	public void run() {
		int retry_timer = INITIAL_RETRY_TIME;

		while (true) {
			if (!open()) {
				try {
					Thread.sleep(retry_timer * 1000);
				} catch (InterruptedException e) {
				}
				retry_timer *= 2;
				if (retry_timer > MAX_RETRY_TIME) {
					retry_timer = MAX_RETRY_TIME;
				}
				continue;
			} else {
				retry_timer = INITIAL_RETRY_TIME;
			}
			byte[] data = null;
			try {
				data = msg_q.take();
			} catch (InterruptedException e) {
				continue;
			}
			if (data == null) {
				continue;
			} else if (data.length == 0) {
				// stopping the service
				break;
			}

			try {
				if (out != null)
					out.write(data);
			} catch (IOException e) {
				try {
					out.close();
				} catch (IOException e1) {
				}
				out = null;
			}
			data = null;
		}
	}

	public void close() {
		try {
			if (out != null) {
				out.flush();
				out.close();
			}
		} catch (IOException e) {
		}
		out = null;
	}
}

/**
 * This is an example of implementing an application service that runs in a
 * different process than the application. Because it can be in another process,
 * we must use IPC to interact with it. The {@link RemoteServiceController} and
 * {@link RemoteServiceBinding} classes show how to interact with the service.
 */
/*s
class CellInfoManager {
	
	private Handler handler = null;
	private Runnable runnable = null;
	
	public void stop()  {
		if(runnable != null && handler != null) {
			handler.removeCallbacks(runnable);
			runnable = null;
			handler = null;
		}
	}
	public boolean start(final Context context, final Handler handler, final long start_time, final int delay_sec, final OutputStream stream) {
		if(runnable != null) {
			return true;
		}
		this.handler = handler; 
		runnable = new Runnable( ) {
			private TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
			public void run ( ) {
				CellLocation cellLocation = (GsmCellLocation)telephonyManager.getCellLocation();
				String networkOperator = telephonyManager.getNetworkOperator();
				if(cellLocation != null && networkOperator!= null && networkOperator.length() > 3) {
					
					String mcc = networkOperator.substring(0, 3);
					String mnc = networkOperator.substring(3);
					
					String data ;
					if(cellLocation instanceof GsmCellLocation) {
						GsmCellLocation loc = (GsmCellLocation) cellLocation;
						data = "GSM " + " " + loc.getCid() + " " + loc.getLac();
					} else if(cellLocation instanceof CdmaCellLocation) {
						CdmaCellLocation loc = (CdmaCellLocation) cellLocation;
						data = "CDMA " + loc.getBaseStationId() + " " + loc.getNetworkId() + " " + loc.getSystemId();
					} else {
						data = "null";
					}
					try {
						stream.write((DataHeader.CELL_DATA.header + " " + (System.currentTimeMillis() - start_time) + " " + mcc + " " + mnc + " " + data + "\r\n").getBytes());
					} catch (IOException e) {}
				}

				handler.postDelayed(this,delay_sec * 1000); 
			}
		};
		
		if(handler.postDelayed(runnable,delay_sec * 1000)) {
			return true;
		} 
		else
		{
			runnable = null;
			return false;
		}
	}
}
*/

public class DataLogger extends Service {
	/**
	 * This is a list of callbacks that have been registered with the service.
	 * Note that this is package scoped (instead of private) so that it can be
	 * accessed more efficiently from inner classes.
	 */
	
	
	final RemoteCallbackList<IRemoteServiceCallback> mCallbacks = new RemoteCallbackList<IRemoteServiceCallback>();

	static final int OUTPUT_BUFFER_MAX_SIZE = 400000;
	static final int uploading_check_delay = 0; //10000 * 60 * 60 ; 
	static final int uploading_delay = (3 * 60 * 60) ; //1 * 60 * 60 = 1 hour
	
	private MySensorManager accel_mgr;
	private MyPowerManager pm;

	private ArrayBlockingQueue<byte[]> write_q = new ArrayBlockingQueue<byte[]>(
			10);
	private FileWriter writer;
	private Mode m = Mode.IDLE;
	private Thread worker;
	private ByteArrayOutputStream out = new ByteArrayOutputStream(OUTPUT_BUFFER_MAX_SIZE + 400);
	private long start;
	private long start_nano;
	
	private NotificationManager notify_mgr;

	private Handler handler = null;
	private MyLocationManager location_mgr = null;
//s	private CellInfoManager cell_mgr = null;

	private long last_upload_time =0;
	
	
	private int idle_count =0;
	
	private int accl_data_length = 0;
	private double[] accl_data = new double[512];
	private long last_check_time;

	private Runnable idle_check_task = new Runnable() {
		private DoubleFFT_1D fft = new DoubleFFT_1D(512);
		public void run() {
			long duration = (System.currentTimeMillis() - last_check_time)/1000;
			if(duration > 20) {
				duration = 20;
			}
			for(int i =0; i < accl_data_length  - 1;++i) {
				accl_data[i] = accl_data[i + 1] - accl_data[i];
			}
			for(int i = accl_data_length -1 ; i < accl_data.length ;++i) {
				accl_data[i] = 0;
			}
			fft.realForward(accl_data);
			
			for(int i =1; i < 256;++i) {
				accl_data[i] = Math.sqrt(accl_data[i*2] * accl_data[i*2] + accl_data[i*2 + 1] * accl_data[i*2 + 1] );
			}
			double max_accl =Math.abs(accl_data[0]);//, max_mag =Math.abs(mag_data[0]);
			for(int i =1; i < 8 * duration;++i) {
				if(max_accl < accl_data[i]) {
					max_accl = accl_data[i];
				}
			}
		}
	};
	
	private void onAccelerationChanged(long timestamp, float x, float y, float z) {
		try {
			out.write((DataHeader.ACCEL_DATA.header + " " + (timestamp - start_nano)/1000000 + " " + (int)(x* 1000) + " " + (int)(y* 1000) + " " + (int)(z* 1000) + "\r\n")
					.getBytes());
		} catch (IOException e1) {
		}

		if (out.size() > OUTPUT_BUFFER_MAX_SIZE) {
			write_q.offer(out.toByteArray());
			out.reset();
		}

		if(accl_data_length < accl_data.length) {
			accl_data[accl_data_length] = Math.sqrt(x* x + y * y + z*z);
			accl_data_length += 1;
		}
		float[] data = { x, y, z };
		final int N = mCallbacks.beginBroadcast();
		for (int i = 0; i < N; i++) {
			try {
				mCallbacks.getBroadcastItem(i).valueChanged(
						Sensor.TYPE_ACCELEROMETER, data);
			} catch (RemoteException e) {
			}
		}
		mCallbacks.finishBroadcast();

	}

	private void initialize_service() {
		start = System.currentTimeMillis();
		start_nano = System.nanoTime();
		
		accel_mgr = new MySensorManager(this, Sensor.TYPE_ACCELEROMETER,
				new SensorEventListener() {
					public void onAccuracyChanged(Sensor sensor, int accuracy) {
					}

					public void onSensorChanged(SensorEvent event) {
						onAccelerationChanged(event.timestamp, event.values[0],	event.values[1], event.values[2]);
					}
				});

		accel_mgr.start();

		// location mgr will notify every 120 seconds if location available
		location_mgr = new MyLocationManager(this, new LocationListener() {
			public void onLocationChanged(Location location) {
				DataLogger.this.onLocationChanged(location);
			}
			public void onProviderDisabled(String provider) {}
			public void onProviderEnabled(String provider) {}
			public void onStatusChanged(String provider, int status,Bundle extras) {}
		} , 120);
		location_mgr.start();
		
		// cell mgr will log cell info every 60 seconds
//s		cell_mgr = new CellInfoManager();
//s		cell_mgr.start(this, handler, start, 60, out);
		
		//UUID
		
			
			
		try {
			out.write((DataHeader.UUID.header + " " + "4860804" + "\r\n").getBytes());
			System.out.println ( DataHeader.UUID.header + " " + "4860804" + "\r\n");
			
			//out.write((DataHeader.UUID.header + " " + get_client_id() + "\r\n").getBytes());
			//System.out.println ( DataHeader.UUID.header + " " + get_client_id() + "\r\n");
		} catch (IOException e) {
		}
		
		try {///s
			
			out.write((DataHeader.INDIVIDUAL.header + " " + get_client_id() + "\r\n").getBytes());
			System.out.println ( DataHeader.INDIVIDUAL.header + " " + get_client_id() + "\r\n");
		} catch (IOException e) {
		}///s
		
		try {
			out.write((DataHeader.START.header + " " + start + "\r\n").getBytes());
		} catch (IOException e) {
		}

		writer = new FileWriter(write_q, getString(R.string.data_folder));
		writer.open(getString(R.string.data_file));
		worker = new Thread(writer);
		worker.start();

	}
	
	
	@Override
	public void onCreate() {

		handler = new Handler();
		
		
		pm = new MyPowerManager(this, PowerManager.PARTIAL_WAKE_LOCK);
		pm.start();
		
		uploading_data_file(false);
		
		initialize_service();
		
		notify_mgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);		
		// Display a notification about us starting.
		showNotification();

		Toast.makeText(this, R.string.data_logger_started, Toast.LENGTH_SHORT)
				.show();
		
	}

	protected void onLocationChanged(final Location location) {		
		try {
			out.write((DataHeader.LOCATION_DATA.header  + " " + (location.getTime() - start) + " " 
		+ location.getLatitude() + " " + location.getLongitude() + "\r\n").getBytes());
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (out.size() > OUTPUT_BUFFER_MAX_SIZE) {
			write_q.offer(out.toByteArray());
			out.reset();
		}
	}
	private void finish_service() {
		long duration = System.currentTimeMillis() - start;
		
		accel_mgr.stop();

		location_mgr.stop();

		
		try {
			out.write((DataHeader.END.header + " " + duration + "\r\n")
					.getBytes());
		} catch (IOException e1) {
		}
		write_q.offer(out.toByteArray());
		write_q.offer(new byte[0]);
		out.reset();
		try {
			worker.join();
		} catch (InterruptedException e) {
		}
		writer.close();
	}
	@Override
	public void onDestroy() {
		finish_service();

		// Cancel the persistent notification.
		notify_mgr.cancel(R.string.data_logger_started);
	//	this.setForeground(false);
		stopForeground(true);
		
		pm.stop();

		// Tell the user we stopped.
		Toast.makeText(this, R.string.data_logger_stopped, Toast.LENGTH_SHORT)
				.show();

		// Unregister all callbacks.
		mCallbacks.kill();
	}

	@Override
	public IBinder onBind(Intent intent) {
		return mBinder;
	}

	@Override
	public boolean onUnbind(Intent intent) {
		return true;
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {

		return START_STICKY;
	}


	private void setMode(Mode m) {
		this.m = m;
		long duration = System.currentTimeMillis() - start;
		try {
			out.write((DataHeader.MODE.header + " " + duration + " "+ m.mode +"\r\n").getBytes());
		} catch (IOException e1) {
		}
	}

	private Mode getMode() {
		return m;
	}

	/**
	 * The IRemoteInterface is defined through IDL
	 */
	private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
		public void registerCallback(IRemoteServiceCallback cb) {
			if (cb != null)
				mCallbacks.register(cb);
		}

		public void unregisterCallback(IRemoteServiceCallback cb) {
			if (cb != null)
				mCallbacks.unregister(cb);
		}

		public void setMode(int m) throws RemoteException {
			Mode mode = Mode.toMode(m);
			if (mode != null)
				DataLogger.this.setMode(mode);
		}

		public int getMode() throws RemoteException {
			return DataLogger.this.getMode().mode;
		}

		public boolean rollover_data_file(String file_name) throws RemoteException {
			return DataLogger.this.rollover_data_file(file_name);
		}
	};

	/**
	 * Show a notification while this service is running.
	 */
	private void showNotification() {
		// In this sample, we'll use the same text for the ticker and the
		// expanded notification
		CharSequence text = getText(R.string.data_logger_started);

		// Set the icon, scrolling text and timestamp
		Notification notification = new Notification(R.drawable.service_on,
				text, System.currentTimeMillis());

		// The PendingIntent to launch our activity if the user selects this
		// notification
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
				new Intent(getString(R.string.logger_controller)), 0);

		// Set the info for the views that show in the notification panel.
		notification.setLatestEventInfo(this,
				getText(R.string.data_logger_label), text, contentIntent);
		notification.flags = Notification.FLAG_FOREGROUND_SERVICE
				| Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;

		// Send the notification.
		startForeground(R.string.data_logger_started, notification);
		notify_mgr.notify(R.string.data_logger_started, notification);
//		this.setForeground(true);
	}

	protected boolean rollover_data_file(String file_name) {
		
		File root = Environment.getExternalStorageDirectory();
		File dest = new File(root + getString(R.string.data_folder) + file_name);
		if (dest.exists()) {
			return false;
		}
		
		finish_service();

		try {
			File org = new File(root + getString(R.string.data_folder) + getString(R.string.data_file));
			if (org.exists()) {
				return org.renameTo(dest);
			}
			return false;
		} finally {
			initialize_service();
		}
	}
	
	public static boolean start_service(final Context context) {
		ComponentName c = context.startService(new Intent(context.getString(R.string.remote_service)));
		if(c==null ) {
			return false;
		}
		else {
			SharedPreferences settings = context.getSharedPreferences(context.getString(R.string.pref_name), 0);
			SharedPreferences.Editor editor = settings.edit();
			editor.putBoolean(context.getString(R.string.auto_start_key), true);
			editor.commit();
			return true;
		}
	}
	public static void stop_service(final Context context) {
		context.stopService(new Intent(context.getString(R.string.remote_service)));
		
		SharedPreferences settings = context.getSharedPreferences(context.getString(R.string.pref_name), 0);
		SharedPreferences.Editor editor = settings.edit();
		editor.putBoolean(context.getString(R.string.auto_start_key), false);
		editor.commit();
	}
	
	
	public static boolean 	is_service_active(final Context context) {
		String remote_service= "fcl.platform.DataLogger";
		return is_service_active(context, remote_service);
	}
	
	
	
	public static boolean 	is_service_active(final Context context, final String remote_service) {
		
		ActivityManager manager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
		for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
			if (remote_service.equals(service.service.getClassName())) {
				return true;
			}
		}
		return false;
	}
	private void uploading_data_file(boolean rollover) {
		String upload_file_name = getString(R.string.upload_file);
		String folder_name = getString(R.string.data_folder);
		final File upload_file = new File(Environment.getExternalStorageDirectory()
				+ folder_name + upload_file_name);
		if(upload_file.exists() && upload_file.length() ==0)
		{
			upload_file.delete();
		}
		if (!upload_file.exists()) {
			if(rollover && writer != null) {
					rollover_data_file(upload_file_name);
			} else {
				final File data_file = new File(Environment.getExternalStorageDirectory()
						+ folder_name + getString(R.string.data_file));
				if(data_file.exists()) {
					data_file.renameTo(upload_file);
				}
			}
		}
		// check uploading status after uploading_check_delay seconds
		if(upload_file.exists() && upload_file.length() > 0) {
			last_upload_time = System.currentTimeMillis();
			startService(new Intent(getString(R.string.upload_service)));
			handler.postDelayed(new Runnable() {
				public void run() {
					if(!is_uploading_finished()) {
						handler.postDelayed(this, uploading_check_delay * 1000);
					} else {
						long delay = last_upload_time - System.currentTimeMillis() + uploading_delay * 1000;
						if(delay >0)
						{
							handler.postDelayed(new Runnable() {
								public void run() {
									uploading_data_file(true);
								}
							}, delay);
						}
						else {
							uploading_data_file(true);
						}
					}
				}
			} , uploading_check_delay * 1000);
		} else {
			handler.postDelayed(new Runnable() {
				public void run() {
					uploading_data_file(true);
				}
			}, uploading_delay * 1000);
		}
	}
	private boolean is_uploading_finished() {
		if(!is_service_active(this, "fcl.platform.Uploader")) {
			String upload_file_name = getString(R.string.upload_file);
			String folder_name = getString(R.string.data_folder);

			final File upload_file = new File(Environment.getExternalStorageDirectory()
					+ folder_name + upload_file_name);
			if(upload_file.exists()) {
				// restart uploading 
				startService(new Intent(getString(R.string.upload_service)));
				return false;
			} else {
				return true;
			}
		}
		// uploading not finished , wait for next checking
		return false;
	}
	private String get_client_id() {
		SharedPreferences settings = getSharedPreferences(getString(R.string.pref_name), 0);
		String client_id = settings.getString(getString(R.string.client_id_key), null);
		
		if(client_id == null) {
			client_id = java.util.UUID.randomUUID().toString();
			SharedPreferences.Editor editor = settings.edit();
			editor.putString(getString(R.string.client_id_key), client_id);
			editor.commit();
		}
		return client_id;
	}
}
